iT邦幫忙

2024 iThome 鐵人賽

DAY 6
0
Python

Python 錦囊密技系列 第 6

【Python錦囊㊙️技6】AOP vs. 裝飾器(Decorator)

  • 分享至 

  • xImage
  •  

前言

依照維基百科的定義,特性導向程式設計(Aspect-oriented programming,AOP)是一種設計模式(Design pattern),它可以自核心程式抽離特定的關注點(Separation of concerns),使程式架構變得更簡潔與模組化,特定的關注點可以是下列功能:

  1. 工作日誌(Log):針對要追蹤的函數,將其名稱及輸入參數寫入工作日誌,以驗證整體作業的執行流程。
  2. 效能衡量(Performance measurement):可以計算每個函數的執行時間,找出作業瓶頸。
  3. 交易(Transaction):將函數內的增/修/刪作業整合成一筆資料庫交易,確保寫入的完整性。
  4. 身份驗證與權限管理(Authentication and Authorization):限制函數必須通過身分驗證與權限管理,才能被執行。
  5. 指定路由及協定:例如Flask網頁開發套件,可指定函數的URL、Get/Post...,又例如另一個網頁開發套件Streamlit可指定函數輸出會存入快取(Cache)。

https://ithelp.ithome.com.tw/upload/images/20240915/20001976CmwOCFgjFw.png
圖一. 自核心程式抽離各種關注點

而Python的裝飾器(Decorator)就是實踐AOP的絕佳工具,它透過宣告的方式,去修飾另一個函數,將被修飾的函數原先要處理的關注點交給Decorator統一處理,讓程式設計師聚焦在商業邏輯的開發,不須要求每一個函數或事件處理都要去檢查使用權限或工作日誌寫入。

使用範例

在開始撰寫Decorator之前,先看看Decorator的使用方式,以提供Flask、Streamlit套件為例。

範例1. 在Flask程式中,利用Decorator(@app.route)指定getname函數的URL為【/getname】,而且只能接收get方式的請求,完全不需額外撰寫程式碼,只須註記或宣告。

from flask import Flask, request
 
app = Flask(__name__)
 
@app.route("/getname", methods=['GET']) # Decorator
def say_hello(): # 事件處理函數
    name = request.args.get('name')
    return "Hello "+name
 
if __name__ == '__main__':
    app.run()

如果不使用【@app.route("/getname", methods=['GET'])】,我們就必須在每一個函數中撰寫判斷式,確認是get或post,並且另外撰寫ㄧ主程式,進行路由的管理。

  1. 檔案儲存為flask_test.py。

  2. 測試:須先安裝Flask,再執行【python flask_test.py】啟動server,在瀏覽器輸入http://localhost:5000/getname?name=michael 。

  3. 執行結果:Hello michael。

  4. 也可以使用Postman測試,改用post,會得到以下錯誤:
    https://ithelp.ithome.com.tw/upload/images/20240920/20001976M6h5XPEPw7.png
    圖二. 使用post送出請求

  5. 執行結果:得到【405 Method Not Allowed】,表/getname不允許post。

範例2. 在Streamlit程式中,指定load_data函數輸出可存入快取(Cache),只要呼叫的輸入參數(url)相同,就會改由Cache取得輸出結果,而不是每次都重新讀取檔案。

@st.cache_data  # caching decorator
def load_data(url):
    df = pd.read_csv(url)
    return df # df 會自動存入Cache

df = load_data("https://github.com/plotly/datasets/raw/master/uber-rides-data1.csv")
st.dataframe(df)

如果不使用【@st.cache_data】,我們就必須在每一個函數中撰寫判斷式,確認是要使用Cache還是重新讀取檔案。

  1. GitHub檔案名稱為streamlit_cache.py。

  2. 測試:須先安裝Streamlit,必須使用下列指令啟動server,Streamlit會自動帶出瀏覽器。

streamlit run streamlit_cache.py
  1. 執行結果:會顯示表格及【Rerun】按鈕,第一次會讀取遠端檔案,需時較久,之後點擊【Rerun】按鈕,會直接讀取Cache,速度非常快。

以上範例說明,使用AOP/Decorator,自核心程式抽離關注點,可以讓我們少寫很多程式,而且程式碼會變得非常簡潔。

Decorator實作

Decorator可以是一個函數(Function)或類別(Class),用以擴展其他函數的功能,但不須更改其他函數的程式碼,只要在函數前面加上註解(Annotation)即可。

範例3. Decorator 簡單實作,檔案名稱為decorator1.py。

  1. Decorator函數會使用inner function(函數內包含另一函數)實作處理邏輯,最後再呼叫inner function,這樣可確保inner function不會被外在程式呼叫,以達到資訊隱藏(Information hiding)的效果。下列程式功能可應用於工作日誌(Log),記錄程式執行的流程,假設一個複雜的系統,一個作業須執行許多函數,可以為每個函數加上【@log_decorator】,這樣就可以顯示執行的流程,對於執行較久的函數還可以額外計算執行時間,找出作業的瓶頸。
# 宣告Decorator函數
def log_decorator(func):
    # inner function
    def wrapper():  # inner function
        print("呼叫函數前")
        func() # 執行say_hello函數
        print("呼叫函數後")
        
    # 呼叫 inner function
    return wrapper

# 為 say_hello 函數加上 Decorator
@log_decorator
def say_hello():
    print("hello !")
    
# 呼叫 say_hello
say_hello()   
  1. 測試:
python decorator1.py
  1. 執行結果:
呼叫函數前
hello !
呼叫函數後

範例4. 顯示執行的流程及執行時間,檔案名稱為decorator2.py。

  1. 宣告3個函數,都加上log_decorator,其中func_b函數計算0至100,000總和,需時較久。
import time

# 宣告Decorator函數
def log_decorator(func):
    # inner function
    def wrapper(): 
        # print("呼叫函數前")
        start_time = time.time()
        func()
        print(f"【{func.__name__}】 執行時間:{(time.time() - start_time)} 秒")        
        # print("呼叫函數後")
    # 呼叫 inner function
    return wrapper

@log_decorator
def func_a():
    print("hello a!")
    
@log_decorator
def func_b():
    total = 0
    for i in range(100_001):
        total += i
    print("hello b!")

@log_decorator
def func_c():
    print("hello c!")
  1. 主程式依序執行func_a、func_b、func_c函數。
if __name__ == "__main__":
    func_a()
    func_b()
    func_c()
  1. 執行結果:可以看出func_b是瓶頸,需時較久,其中func.__name__可取得函數名稱。
hello a!
【func_a】 執行時間:0.0009965896606445312 秒
hello b!
【func_b】 執行時間:0.006021738052368164 秒
hello c!
【func_c】 執行時間:0.0010008811950683594 秒

範例5. 顯示函數的參數名稱及其內容,檔案名稱為decorator3.py。

  1. 使用inspect.signature(func).parameters.keys()取得參數名稱,*arg1取得參數值,同時計算函數執行時間,找出作業的瓶頸。
import time, inspect

# 宣告Decorator函數
def log_decorator(func):
    # inner function
    def wrapper(*arg1, **arg2): 
        # print("呼叫函數前")
        start_time = time.time()
        func(*arg1, **arg2)
        print(f"【{func.__name__}】執行時間:{(time.time() - start_time)} 秒")        
        print(f"\t參數:", end=' ') # {inspect.signature(func)}: {arg1}")
        for name, value in zip(inspect.signature(func).parameters.keys(), arg1):
            print(f"{name}:{value}", end='\t')
            
        print(f"\t\t")        
        # print("呼叫函數後")
    # 呼叫 inner function
    return wrapper

@log_decorator
def func_a(x):
    print("hello a!")
    
@log_decorator
def func_b(x1, x2):
    total = 0
    for i in range(100_001):
        total += i
    print("hello b!")

@log_decorator
def func_c(y1, y2, y3):
    print("hello c!")
  1. 主程式依序執行func_a、func_b、func_c函數。
if __name__ == "__main__":
    func_a(1)
    func_b(2, 3)
    func_c(4, 5, 6)    
  1. 執行結果:可以取得參數名稱及參數值。
hello a!
【func_a】執行時間:0.0010006427764892578 秒
        參數: x:1
hello b!
【func_b】執行時間:0.00599980354309082 秒
        參數: x1:2     x2:3
hello c!
【func_c】執行時間:0.0009999275207519531 秒
        參數: y1:4     y2:5    y3:6

結語

本篇說明AOP概念、套件如何使用Decorator以及如何撰寫Decorator應用在工作日誌(Log)上,下一篇會繼續介紹Decorator更多的功能,Happy coding!!

本系列的程式碼會統一放在GitHub,本篇的程式放在src/6資料夾,歡迎讀者下載測試,如有錯誤或疏漏,請不吝指正。


上一篇
【Python錦囊㊙️技5】來寫一個直譯器(Interpreter)吧!
下一篇
【Python錦囊㊙️技7】裝飾器(Decorator)深入研究
系列文
Python 錦囊密技14
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言